크래프톤 정글 입학시험 회고록 & 문제 해결


지난 7월 중순, 1년 2개월 동안 재직하던 IT회사를 최근에 나오게 되었다. 29살이라는 나이에 많은 고민을 한 끝에 결정했지만, 그래도 내 인생, 나만의 목표를 위해서는 나오는게 맞다고 생각되어 결정하게 되었다.

회사를 그만두게 된 이유는 여러 가지 이유가 있지만 가장 아쉬웠던 점은 내가 생각한 ‘개발자’라는 직업의 범위를 벗어났기 때문이다. 물론 회사의 경영 사정도 잘 알고 있고, 설렵한지 2년도 채 안된 스타트업이었기 때문에 ‘생존’이 가장 최우선 되어야 한다. 재직 중에도 “시간이 지나면 상황이 나아지겠지, 달라지겠지”라고 되뇌이며 열심히 회사가 성장할 수 있도록 주어진 일에 집중했다. 하지만 스타트업에서 ‘내가 할 수 있는 업무의 범위가 넓고, 다양한 시도를 해 볼 수 있을 것’이라는 첫 기대와 달리 사업의 특성상 제약적인 사항들이 많았고, 이대로 시간이 지나가게 된다면 꿈꿔오던 개발자라는 커리어에 도달하지 못할 것 같다는 생각을 하게 되었다. 개발자가 되기 위해서 힘들더라도 다시 나와 취업 준비를 하는게 맞다고 생각했다.

회사를 그만두기 전에 취업 준비를 다시 하게 된다면 혼자서 준비하기 보단 같은 목표를 위해 모인 사람들과 다 같이 으쌰으쌰! 하면서 준비하면 힘도 나고 오랫동안 열정을 가진 채 준비를 할 수 있을 것이라고 생각했다. 하지만 이미 나는 국비지원교육을 수강한 이력이 있었다. 그럼 혼자서 취업 준비를 해야하나 싶었는데… 그때!! 크래프톤 정글 프로그램을 찾을 수 있게 되었다.

크래프톤 정글 프로그램을 꼼꼼하게 알아보았는데, 우선 ‘좋은 개발자’가 되기 위해 자기주도적 학습과 SW에만 몰입할 수 있는 인재들을 모집한다는게 가장 끌렸다. 이전에도 국비지원교육을 수강하면서 동료들한테 ‘열정’이라는 단어를 많이 들었는데, 열정 넘치는 사람들 곁에서 나도 좋은 에너지를 받아가고 같이 열정을 불태우면 시너지 효과가 엄청날 것이라고 생각했기 때문이다.

입학 서류를 지원하고 2주간 공부할 수 있는 입학 시험 준비자료를 준다. 2주간의 몰입도 높은 스스로의 학습을 통해 크래프톤 정글 프로그램에 적합한지 스스로 점검해 보는 것이다. 이런 부분도 취지가 너무 좋다고 생각한다. 정말 사람은 마음가짐에 따라 행동한다고 생각하는데, 이런 점검 과정을 통해 나 스스로를 체크해 볼 수 있기 때문이다. 시험은 7시간동안 진행하였고, 결과는 아쉽게 하나의 기능을 마저 구현하지 못했다. 기능을 모두 구현하지 못했다는 것이 정말 아쉬웠지만 2주간의 스스로 몰입도 높은 학습을 통해 ‘나 스스로 노력하는 것에 따라 단기간에 얼마나 빠르게 성장할 수 있는지, 희망을 보았던 아주 좋은 경험’ 이었다고 생각한다. (시험이 종료된 이후 주말동안 시간을 투자해 문제를 모두 해결하였다.)

꼭 이 프로그램을 위해서가 아니라, 앞으로 인생을 살아가는데도 나 스스로 인생을 개척해 나갈 수 있도록 이 경험과 마음가짐을 기억하고 앞으로 나아가야겠다.

아래는 정글 시험 내용을 기억하기 위해 정리해 본 내용입니다. 문제가 될 시 삭제하도록 하겠습니다.

프로젝트 설계

1. 포스팅 조회(GET)

  • 요청 정보

    • 요청 URL = /api/list, 요청 방식 = GET
    • 요청 데이터: 없음
  • 서버가 제공할 기능
  • DB에 저장되어 있는 모든(제목, 내용, 좋아요 갯수) 정보 보내주기

    • DB 데이터를 보내줄 때 좋아요 갯수를 기준으로 내림차순 정렬
  • 응답 데이터
  • 포스팅 정보(제목, 내용, 좋아요 갯수)

    • (JSON 형식) ‘result’ == ‘success’, ‘posting’ 포스팅 정보

2. 포스팅 만들기(CREATE)

  • 요청 정보

    • 요청 URL = /api/post, 요청 방식 = POST
    • 요청 데이터: 제목(titlegive), 내용(contentsgive)
  • 서버가 제공할 기능
  • 포스팅 정보를 모두 DB에 저장(제목, 내용, 좋아요 갯수 - default:0)
  • 응답 데이터

    • API가 정상적으로 작동하는지 클라이언트에게 알려주기 위해서 성공 메시지 보내기
    • (JSON 형식) ‘result’ = ‘success’
    • 성공 시 페이지 reload

3. 포스팅 수정하기 (UPDATE)

  • 요청 정보

    • 요청 URL = /api/update, 요청 방식 = POST
    • 요청 데이터: DB 객체 id 값(idgive), 업데이트 제목(newtitlegive), 업데이트 내용(newcontents_give)
  • 서버가 제공할 기능

    • 해당 포스팅 DB 객체 id 값(idgive)를 찾아 UPDATE(newtitlegive, newcontents_give)
  • 응답 데이터
  • (JSON 형식) ‘result’ = ‘success’

    • 성공 시 페이지 reload

4. 포스팅 삭제하기(DELETE)

  • 요청 정보

    • 요청 URL = /api/delete, 요청 방식 = POST
    • 요청 데이터: DB 객체 id 값(id_give)
  • 서버가 제공할 기능

    • 해당 포스팅 DB 객체 id 값(id_give)를 찾아 DB에서 해당 데이터를 삭제
  • 응답 데이터

    • API가 정상적으로 작동하는지 클라이언트에게 알려주기 위해서 성공 메시지 보내기
    • (JSON 형식) ‘result’ = ‘success’
    • 성공 시 페이지 reload

      5. 좋아요 기능(UPDATE)

  • 요청 정보

    • 요청 URL = /api/like, 요청 방식 = POST
    • 요청 데이터: DB 객체 id 값(id_give)
  • 서버가 제공할 기능

    • 해당 포스팅 DB 객체 id 값(id_give)를 찾아 기존 좋아요 갯수에 like + 1를 추가 후 DB 업데이트
  • 응답 데이터

    • API가 정상적으로 작동하는지 클라이언트에게 알려주기 위해서 성공 메시지 보내기
    • (JSON 형식) ‘result’ = ‘success’
    • 성공 시 페이지 reload

구현하면서 겪은 문제 해결

각각의 포스팅 정보를 구별하기 위한 데이터 처리

포스팅 CRUD API 작성 중 각각의 포스팅이 어떤건지 알아내기 위해서 MongoDB의 _id 값이 필요했습니다.(RDB의 Primary Key 같은 유일한 값과 비슷한 개념입니다.) 처음 구현했을 땐 수정되어야 할 포스팅의 제목을 가지고 서버에 제목 데이터(titlegive)를 보내 그 객체를 업데이트 하는 방식으로 하였으나, 이렇게 하면 제목이 같은 포스팅들은 `pymongo - findone()함수로 호출 시(조건에 맞는 첫 번째 데이터를 찾음) 엉뚱한 객체를 찾을 수 있으므로 이를 해결하기 위해 DB 데이터에서 유일한 값인_id`를 넘겨주기로 하였습니다.

💡

BSON 란?

Binary JSON의 약어로 Binary로 인코딩 된 JSON을 의미합니다.

JSON TypeError(MongoDB의 _id 값) 문제

  • MongoDB의 _id를 넘기기 위해서 처음에는 list(db.collections.find())이런식으로 작성하였으나 JSON 형태로 데이터를 보내주기 위한 flask 함수인 jsonify를 사용할 시 TypeError가 발생했습니다.(TypeError: Object of type ObjectId is not JSON serializable) ObjectId 유형의 객체는 직렬화가 불가능하다는 메시지입니다. MongoDB에서 document가 BSON 형태로 저장되고 있기 때문에 JSON 형태로 변환이 불가능한 것입니다.

이를 해결하기 위해!

  1. python - from bson.json_util import dumps 함수를 이용하여 데이터를 JSON(str) 형태로 변경
  2. 위의 상태만 진행했을 때, Dictionary 형태가 아닌 String 형태이기 때문에
  3. javasciprt - JSON.parse()를 이용해 JSON 형태의 데이터를 Dictionary 형태로 변경해 필요한 클라이언트 부분에 코딩 진행
  4. 또한 서버측에서 pymongo 모듈을 통해 DB의 _id 값으로 조회, 삭제, 업데이트 등을 할 때 from bson.objectid import ObjectId 함수를 이용해서 _id 값을 반환 받아야 정상적으로 개체의 16진수 문자열 표현을 얻을 수 있습니다.
  5. 아래 app.py 의 사용 예시를 보여드리겠습니다.
포스팅 조회 API
from bson.json_util import dumps
from bson.objectid import ObjectId

...

@app.route('/api/list', methods=['GET'])
def get_articel():
    # 1. 모든 document 찾기
    result = list(db.memos.find().sort('like', -1))
    return jsonify({'result': 'success', 'posting': dumps(result)})
포스팅 수정 API
from bson.json_util import dumps
from bson.objectid import ObjectId

...

@app.route('/api/update', methods=['POST'])
def update_article():
    id_receive = request.form['id']
    update_title_receive = request.form['new_title_give']
    update_contents_receive = request.form['new_contents_give']
    db.memos.update_one({'_id': ObjectId(id_receive)},
                        {'$set': {'title_receive': update_title_receive, 'contents_receive': update_contents_receive}})
    return jsonify({'result': 'success'})
포스팅 삭제 API
from bson.json_util import dumps
from bson.objectid import ObjectId

...

@app.route('/api/delete', methods=['POST'])
def delete_article():
    # 1. 클라이언트가 전달한 id_give id_receive 변수에 넣습니다.
    id_receive = request.form['id_give']
    # 2. id_receive와 일치하는 id를 제거합니다.
    db.memos.delete_one({'_id': ObjectId(id_receive)})
    # 3. 성공하면 success 메시지를 반환합니다.
    return jsonify({'result': 'success'})

토글 버튼 클릭 시, Element id 값 지정

각 포스팅 박스 마다 ‘수정 ‘버튼 클릭 시 자바스크립트로 구현한 토글 함수가 작동해야 하는데, 첫 번째의 박스만 작동하는 문제가 있었습니다. 개발자 도구와 콘솔을 이용해 원인을 분석한 결과 한 페이지 내에서선 Element의 ID가 고유해야 하는데 DOM 객체 모두가 같은 ID값을 가지고 있었고, 오타로 적힌 부분이 있었습니다. 이를 해결하기 위해 각 DOM 객체마다 다른 ID 값을 부여해 주었고, 콘솔을 통해 DOM 객체가 선택되는지 확인 후 선택되지 않는걸 확인하고 오타를 찾아 해결하였습니다.

이를 해결하기 위해!

문제 접근 방식은 다음과 같습니다. 최대한 문제를 잘게 쪼개서 해결하였습니다.

  1. 각 DOM 객체마다 데이터(index)가 넘어오는지 확인
  2. 콘솔을 이용해 각 DOM 객체가 선택되는지 확인
  3. 콘솔을 이용해 DOM의 속성에 접근해보기

    • $("#TEST").css('display')
        // '수정 버튼 클릭 시 토클'
function cardToggle(index) {
    let status = $(`#toggle-origin${index}`).css('display');

    if (status == 'block') {
        $(`#toggle-new${index}`).css('display', 'block');
        $(`#toggle-origin${index}`).css('display', 'none');
        $(`#update-title${index}`).attr('value', $(`#id-title${index}`).text());
    } else {
        $(`#toggle-new${index}`).css('display', 'none');
        $(`#toggle-origin${index}`).css('display', 'block');
            }
}

완성 코드

app.py

from flask import Flask, render_template, jsonify, request
from pymongo import MongoClient
from bson.json_util import dumps
from bson.objectid import ObjectId


app = Flask(__name__)

db = MongoClient('localhost', 27017).dbjungle
# AWS 서버 배포 주소
# db = MongoClient('mongodb://test:test@3.34.142.63', 27017).dbjungle


@app.route('/')
def home():
    return render_template('index.html')


@app.route('/api/list', methods=['GET'])
def get_articel():
    # 1. 모든 document 찾기
    result = list(db.memos.find().sort('like', -1))
    return jsonify({'result': 'success', 'posting': dumps(result)})


@app.route('/api/post', methods=['POST'])
def post_article():
    # 1. 클라이언트로부터 데이터를 받기
    title_receive = request.form['title_give']
    contents_receive = request.form['contents_give']

    doc = {
        'title_receive': title_receive,
        'contents_receive': contents_receive,
        'like': 0
    }

    # 2. mongoDB에 데이터를 넣기
    db.memos.insert_one(doc)

    return jsonify({'result': 'success'})


@app.route('/api/update', methods=['POST'])
def update_article():
    id_receive = request.form['id']
    update_title_receive = request.form['new_title_give']
    update_contents_receive = request.form['new_contents_give']
    db.memos.update_one({'_id': ObjectId(id_receive)},
                        {'$set': {'title_receive': update_title_receive, 'contents_receive': update_contents_receive}})
    return jsonify({'result': 'success'})


@app.route('/api/delete', methods=['POST'])
def delete_article():
    # 1. 클라이언트가 전달한 id_give id_receive 변수에 넣습니다.
    id_receive = request.form['id_give']
    # 2. id_receive와 일치하는 id를 제거합니다.
    db.memos.delete_one({'_id': ObjectId(id_receive)})
    # 3. 성공하면 success 메시지를 반환합니다.
    return jsonify({'result': 'success'})


@app.route('/api/like', methods=['POST'])
def like_star():
    # 1. 클라이언트가 전달한 id_give를 id_receive 변수에 넣습니다.
    id_receive = request.form['id_give']
    # 2. id_receive와 일치하는 id를 찾습니다.
    article = db.memos.find_one({'_id': ObjectId(id_receive)})
    # 3. article의 like 에 1을 더해준 new_like 변수를 만듭니다.
    new_like = article['like'] + 1
    # 4. 해당 article의 like 를 new_like로 변경합니다.
    db.memos.update_one({'_id': ObjectId(id_receive)},
                        {'$set': {'like': new_like}})
    # 5. 성공하면 success 메시지를 반환합니다.
    return jsonify({'result': 'success'})


if __name__ == '__main__':
    app.run('0.0.0.0', port=5000, debug=True)

index.html

<!Doctype html>
<html lang="ko">

<head>
    <!-- Required meta tags -->
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">

    <!-- Bootstrap CSS -->
    <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@4.6.0/dist/css/bootstrap.min.css"
        integrity="sha384-B0vP5xmATw1+K9KRQjQERJvTumQW0nPEzvF6L/Z6nronJ3oUOFUFpCjEUQouq2+l" crossorigin="anonymous">

    <!-- JS -->
    <script src="https://ajax.googleapis.com/ajax/libs/jquery/3.6.0/jquery.min.js"></script>

    <!-- 구글폰트 -->
    <link href="https://fonts.googleapis.com/css?family=Stylish&display=swap" rel="stylesheet">


    <title>크래프톤정글 | 나홀로 메모장</title>

    <!-- style -->
    <style type="text/css">
        * {
            font-family: "Stylish", sans-serif;
        }

        span {
            background-color: gray;
            color: whitesmoke;
        }

        .wrap {
            width: 900px;
            margin: auto;
        }

        .comment {
            color: blue;
            font-weight: bold;
        }

        #post-box {
            width: 500px;
            border-radius: 5px;
        }
    </style>
    <script>
        $(document).ready(function () {
            showArticles();
        });

        function showArticles() {
            $.ajax({
                type: "GET",
                url: "/api/list",
                data: {},
                success: function (response) {
                    // JSON 데이터를 객체로 변경
                    let articles = JSON.parse(response['posting']);
                    for (let i = 0; i < articles.length; i++) {
                        let title = articles[i]['title_receive'];
                        let contents = articles[i]['contents_receive'];
                        let like = articles[i]['like'];
                        let id = articles[i]['_id']['$oid'];

                        let tempHtml = `<div class="card">
                                            <div id="toggle-new${i}" style="padding: 10px; display: none">
                                                <div class="form-group">
                                                    <input id="update-title${i}" value="" class="form-control new-title">
                                                </div>
                                                <div class="form-group">
                                                    <textarea id="update-contents${i}" class="form-control new-text" rows="2" >${contents}</textarea>
                                                </div>
                                                <button onclick="updateArticle('${id}, ${contents}, ${i}')" type="button" class="btn btn-success save-button">저장</button>
                                            </div>

                                            <div id="toggle-origin${i}" class="card-body">
                                                <p id="id-title${i}" class="card-title" alt="Paragraph">${title}</p>
                                                <p id="id-contents${i}" class="card-text">${contents}</p>
                                                <p class="card-likes">${like}</p>
                                                <button onclick="cardToggle(${i})" type="button" class="btn btn-info edit-button">수정</button>
                                                <button onclick="deleteArticle('${id}')" type="button" class="btn btn-danger delete-button">삭제</button>
                                                <a href="#" onclick="likeStar('${id}')" class="link-like">좋아요!</a>
                                            </div>
                                        </div>`;
                        $("#card-list").append(tempHtml);
                    }
                }
            })
        }

        function postArticle() {
            // 제목, 내용 빈 값 체크
            let checkTitle = $("#memo-title").val();
            let checkContents = $("#memo-content").val();
            if (checkTitle == '') {
                alert("제목을 입력해주세요.");
                return
            } else if (checkContents == '') {
                alert("내용을 입력해주세요.");
                return
            }

            let title = $("#memo-title").val();
            let contents = $("#memo-content").val();

            // 1. POST 방식으로 메모 생성 요청하기
            $.ajax({
                type: "POST", // POST 방식으로 요청
                url: "/api/post", // /api/posting라는 url에 요청
                data: { 'title_give': title, 'contents_give': contents }, // 데이터를 주는 방법
                success: function (response) { // 성공하면
                    if (response["result"] == "success") {
                        // 2. 성공 시 페이지 새로고침하기
                        window.location.reload();
                    }
                }
            });
        }

        // '수정 버튼 클릭 시 토클'
        function cardToggle(index) {
            let status = $(`#toggle-origin${index}`).css('display');

            if (status == 'block') {
                $(`#toggle-new${index}`).css('display', 'block');
                $(`#toggle-origin${index}`).css('display', 'none');
                $(`#update-title${index}`).attr('value', $(`#id-title${index}`).text());
            } else {
                $(`#toggle-new${index}`).css('display', 'none');
                $(`#toggle-origin${index}`).css('display', 'block');
            }

        }

        function updateArticle(title, contents, index) {
            let temp = title.split(', ');
            let id = temp[0]
            let new_title = $(`#update-title${temp[2]}`).val();
            let new_contents = $(`#update-contents${temp[2]}`).val();
            $.ajax({
                type: 'POST',
                url: '/api/update',
                data: {},
                data: { 'id': id, 'new_title_give': new_title, 'new_contents_give': new_contents },
                success: function (response) {
                    if (response['result'] == 'success') {
                        // 2. 성공 시 페이지 새로고침하기
                        window.location.reload();
                    }
                }
            });
        }

        function deleteArticle(id) {
            $.ajax({
                type: 'POST',
                url: '/api/delete',
                data: { 'id_give': id },
                success: function (response) {
                    if (response['result'] == 'success') {
                        alert('삭제 완료!');
                    }
                    window.location.reload();
                }
            });
        }

        function likeStar(id) {
            $.ajax({
                type: 'POST',
                url: '/api/like',
                data: { 'id_give': id },
                success: function (response) {
                    if (response['result'] == 'success') {
                        alert("좋아요 완료!");
                    }
                    window.location.reload();
                }
            });
        }
    </script>
</head>

<body>
    <div class="wrap">
        <div class="jumbotron">
            <h1 class="display-4">나홀로메모장 <span>ver2.0</span></h1>
            <div id="post-box" class="form-post" style="display:block">
                <div>
                    <div class="form-group">
                        <input id="memo-title" class="form-control" placeholder=" 제목을 입력하세요">
                    </div>
                    <div class="form-group">
                        <textarea id="memo-content" class="form-control" rows="2" placeholder="내용을 입력하세요"></textarea>
                    </div>
                </div>
            </div>
            <button type="submit" class="btn btn-primary" onclick="postArticle()">저장하기</button>
        </div>
        <div id="card-list" class="card-columns">
        </div>
    </div>
</body>

</html>


Hello, I'm@nickhealthy
개발자를 꿈꾸고, 파이썬과 클라우드에 관심이 많은 비전공자

Github